Skip to content

Use cattrs hooks for nested recursive classes in msgspec - #803

Open
digit50 wants to merge 2 commits into
python-attrs:mainfrom
digit50:fix/msgspec-recursive-hooks
Open

digit50 wants to merge 2 commits into
python-attrs:mainfrom
digit50:fix/msgspec-recursive-hooks

Conversation

@digit50

@digit50 digit50 commented Oct 17, 2026

Copy link
Copy Markdown

closes #795

With the msgspec converter, msgspec_attrs_unstructure_factory generated a class's unstructuring hook while its recursion probe still held classes in the working set. Field handlers that led back to a probed class were looked up during that time and came back as msgspec.to_builtins, so nested instances skipped registered hooks and lost private attributes. I changed both generation points (the private-attribute path and the one after the probe) so that, while any probe is active, the factory returns a hook that generates the real one on its first call, after the outermost probe has finished. Outside a probe the hook is generated immediately as before. Returning converter.unstructure instead is not enough: a namedtuple field caches the hooks of its element types during the probe, and a private class reached that way would then dispatch to itself forever.

The new tests in tests/preconf/test_msgspec_cpython.py cover a self-recursive class with a hooked datetime field, a mutually recursive pair where one class has a private attribute, and a private class reached through a namedtuple field. The first two fail on main with the reported output.

The msgspec attrs hook factory generated unstructuring hooks while its recursion probe was still active, so handlers for fields leading back to a class being probed were baked in as msgspec.to_builtins. Hook generation is now deferred to the first call whenever a probe is active, so every level of a recursive class goes through the same cattrs hook.
@codspeed

codspeed Bot commented Oct 19, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 11.35%

ℹ️ 64 benchmarks have no flame graph

The flamegraph could not be generated because we failed to resolve the symbols in the profiling trace. This happens when we fail to harvest information about language frames.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 4 regressed benchmarks
✅ 60 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ ℹ️ test_unstructure_simple_int_enum[OrjsonConverter] 16 µs 18.2 µs -12.39%
❌ ℹ️ test_unstructure_simple_int_enum[MsgspecJsonConverter] 16.1 µs 18.2 µs -11.76%
❌ ℹ️ test_unstructure_simple_int_enum[Converter] 16.3 µs 18.4 µs -11.18%
❌ ℹ️ test_unstructure_simple_int_enum[BaseConverter] 17.2 µs 19.1 µs -10.05%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing digit50:fix/msgspec-recursive-hooks (1fe118f) with main (2c6cf5b)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (73fa273) during the generation of this report, so 2c6cf5b was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@Tinche

Tinche commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Pretty sure the performance regressions are unrelated. Can you add a link to the PR to the history entry?

Also I don't think this is the end of the story - for example, Event | None still won't work. I will think on a more general solution in the future.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

msgspec converter skips hooks and drops private fields in nested recursion

2 participants